package cc.jq1024.domain.strategy.service.armory.impl;

import cc.jq1024.domain.strategy.model.entity.StrategyAwardEntity;
import cc.jq1024.domain.strategy.model.entity.StrategyEntity;
import cc.jq1024.domain.strategy.model.entity.StrategyRuleEntity;
import cc.jq1024.domain.strategy.repository.IStrategyRepository;
import cc.jq1024.domain.strategy.service.armory.IStrategyArmory;
import cc.jq1024.domain.strategy.service.armory.IStrategyDispatch;
import cc.jq1024.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.SecureRandom;
import java.util.*;

/**
 * @Author: jia-qiang ljq1024.cc
 * @desc: 策略装配库实现类(兵工厂), 负责初始化策略计算
 * @Date: 2024-04-11-22:21
 */
@Slf4j
@Service
public class StrategyArmoryDispatchImpl implements IStrategyArmory, IStrategyDispatch {

    @Resource
    IStrategyRepository strategyRepository;

    /**
     * 装配抽奖策略
     *
     * @param strategyId 策略ID
     */
    @Override
    public boolean assembleLotteryStrategy(Long strategyId) {
        try {
            // 1. 查询这个策略的配置信息(奖品,规则等...)
            List<StrategyAwardEntity> strategyAwardEntities = strategyRepository.queryStrategyAwardList(strategyId);

            // 2. 缓存产品库存， 【用于decr库存扣减】
            for (StrategyAwardEntity strategyAwardEntity : strategyAwardEntities) {
                // 拿到奖品ID
                Long awardId = strategyAwardEntity.getAwardId();
                // 奖品库存
                Integer awardCount = strategyAwardEntity.getAwardCount();
                cacheStrategyAwardCount(strategyId, awardId, awardCount);
            }


            // 3.1 进行策略装配 - 全量抽奖概率，查找表等
            assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities);
            // 3.2 进行权重规则配置 - 适用于 rule_weight 权重规则配置
            StrategyEntity strategyEntity = strategyRepository.queryStrategyEntityByStrategyId(strategyId);
            String ruleWeight = strategyEntity.getRuleWeight();
            if (null == ruleWeight) return true;

            // 查询策略规则实体
            StrategyRuleEntity strategyRuleEntity = strategyRepository.queryStrategyRuleEntity(strategyId, ruleWeight);
            Map<String, List<Long>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues();
            // 获取所有键的集合
            Set<String> keys = ruleWeightValueMap.keySet();
            for (String key : keys) {
                // 获取key 的所有键
                List<Long> ruleWeightValues = ruleWeightValueMap.get(key);
                // 深拷贝 strategyAwardEntities 集合， 目的是为了不破坏原有的数据，因为该数据可能不是只有这里使用
                ArrayList<StrategyAwardEntity> strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities);
                // 移除不在 ruleWeightValues 集合中的奖品ID
                strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId()));
                // 再次将新的策略集合存到 redis
                assembleLotteryStrategy(String.valueOf(strategyId).concat("_").concat(key), strategyAwardEntitiesClone);
            }

        } catch (Exception e) {
            log.error("装配抽奖策略出错: ", e);
            throw new AppException(e.getMessage());
        }
        return true;
    }

    /**
     * 装配抽奖策略配置「触发的时机可以为活动审核通过后进行调用」
     *
     * @param activityId 活动ID
     * @return 装配结果
     */
    @Override
    public boolean assembleLotteryStrategyByActivityId(Long activityId) {
        // 通过活动ID查询策略ID
        Long strategyId = strategyRepository.queryStrategyIdByActivityId(activityId);
        // 使用策略ID进行策略装配
        return assembleLotteryStrategy(strategyId);
    }

    /**
     * 缓存产品库存
     */
    private void cacheStrategyAwardCount(Long strategyId, Long awardId, Integer awardCount) {
        strategyRepository.cacheStrategyAwardCount(strategyId, awardId, awardCount);
    }

    /**
     * 专门用于策略装配工作
     */
    private void assembleLotteryStrategy(String key, List<StrategyAwardEntity> strategyAwardEntities) {
        // 1. 获取最小概率值
        BigDecimal minAwardRate = strategyAwardEntities.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .min(BigDecimal::compareTo)
                .orElse(BigDecimal.ZERO);
        log.info("策略key: {} 对应的【最小抽奖概率: {}】", key, minAwardRate);
        if (minAwardRate.compareTo(BigDecimal.ZERO) <= 0) {
            throw new RuntimeException("invalid minAwardRate! because it is le zero: " + minAwardRate);
        }

        // 2. 获取概率总和
        BigDecimal totalAwardRate = strategyAwardEntities.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalAwardRate.compareTo(BigDecimal.ZERO) <= 0) {
            throw new RuntimeException("invalid totalAwardRate! because it is le zero: " + totalAwardRate);
        }
        log.info("策略key: {} 对应的【抽奖概率总和: {}】", key, totalAwardRate);

        // 3. 用 概率总和 / 最小概率值 -> 1 / 0.0001 = 10000 获取概率范围 百分位?千分位?万分位?
        // 计算两个BigDecimal类型的数值totalAwardRate和minAwardRate的商，并将结果保留0位小数，使用CEILING模式进行向上取整
        BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING);
        log.info("策略key: {} 对应的【粗略抽奖概率范围: {}】", key, rateRange);
        // 4. 填充策略的概率-奖品查找表
        List<Long> strategyAwardSearchRateTable = getStrategyAwardSearchRateTable(rateRange, strategyAwardEntities);
        log.info("策略key: {} 对应的【查找表】填充完毕", key);

        // 5. 使查找表乱序
        Collections.shuffle(strategyAwardSearchRateTable);
        log.info("策略key: {} 对应的【查找表】乱序完毕", key);

        // 6. 放到 map 中
        HashMap<Integer, Long> shuffleStrategyAwardSearchRateTable = new HashMap<>();
        for (int i = 0; i < strategyAwardSearchRateTable.size(); i++) {
            shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTable.get(i));
        }
        log.info("策略key: {} 对应的【查找表】map转换完毕", key);
        log.info("策略key: {} 对应的【最终抽奖概率范围: {}】", key, shuffleStrategyAwardSearchRateTable.size());

        // 7. 存储到 redis 这里使用 shuffleStrategyAwardSearchRateTable 而不是 rateRange 是为了避免总概率不为 1 的时候会出现不命中缓存表的问题
        strategyRepository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable);
        log.info("策略key: {} 对应的【概率范围】与【查找表】已存储到redis", key);
    }

    /**
     * 填充策略的概率-奖品查找表
     */
    private List<Long> getStrategyAwardSearchRateTable(BigDecimal rateRange, List<StrategyAwardEntity> strategyAwardEntities) {
        List<Long> strategyAwardSearchRateTable = new ArrayList<>(rateRange.intValue());
        for (StrategyAwardEntity strategyAwardEntity : strategyAwardEntities) {
            // 拿到奖品ID
            Long awardId = strategyAwardEntity.getAwardId();
            // 奖品概率
            BigDecimal awardRate = strategyAwardEntity.getAwardRate();
            // 计算出该概率需要占用查找表的空间
            int size = rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue();
            for (int i = 0; i < size; ++i) {
                strategyAwardSearchRateTable.add(awardId);
            }
            log.info("策略ID: {} 下的奖品ID: {} 的获奖概率为: {} 在查找表中的数量为: {}", strategyAwardEntity.getStrategyId(),
                    strategyAwardEntity.getAwardId(), strategyAwardEntity.getAwardRate(), size);
        }
        return strategyAwardSearchRateTable;
    }

    /**
     * 没有装配权重的情况
     * 生成抽奖时的随机数,用于在查找表中获取抽奖奖品
     */
    @Override
    public Long getRandomAwardId(Long strategyId) {
        // 1. 获取随机数生成范围
        Integer rateRange = strategyRepository.getRateRange(strategyId);
        if (rateRange == null) {
            throw new RuntimeException("rateRange is not in redis!");
        }
        // 2. 生成随机数(概率)
        int randomRate = new SecureRandom().nextInt(rateRange);
        // 3. 返回对应的奖品 ID  // "assemble" 是一个动词，意思是"组装"或"装配"
        return strategyRepository.getStrategyAwardAssemble(strategyId, randomRate);
    }

    /**
     * 装配权重的情况
     * 生成抽奖时的随机数,用于在查找表中获取抽奖奖品
     */
    @Override
    public Long getRandomAwardId(Long strategyId, String ruleWeightValue) {
        String key = String.valueOf(strategyId).concat("_").concat(ruleWeightValue);
        // 1. 获取随机数生成范围
        Integer rateRange = strategyRepository.getRateRange(key);
        if (rateRange == null) {
            throw new RuntimeException("rateRange is not in redis!");
        }
        // 2. 生成随机数(概率)
        int randomRate = new SecureRandom().nextInt(rateRange);
        // 3. 返回对应的奖品 ID  // "assemble" 是一个动词，意思是"组装"或"装配"

        return strategyRepository.getStrategyAwardAssemble(key, randomRate);

    }

    /**
     * 根据策略ID和奖品ID，扣减奖品缓存库存
     *
     * @param strategyId  策略ID
     * @param awardId     奖品ID
     * @param endDateTime 活动结束时间
     * @return 扣减结果
     */
    @Override
    public Boolean subtractionAwardStock(Long strategyId, Long awardId, Date endDateTime) {
        return strategyRepository.subtractionAwardStock(strategyId, awardId, endDateTime);
    }

}
